Importing of Scenarios now works, creating any needed Agents by global guid

Andrew Cantino 10 years ago
parent
commit
64564eb120

+ 1 - 0
app/assets/stylesheets/application.css.scss.erb

@@ -158,6 +158,7 @@ h2 .scenario, a span.label.scenario {
158 158
 }
159 159
 
160 160
 // Bootstrappy color styles
161
+
161 162
 .color-danger {
162 163
   color: #d9534f;
163 164
 }

+ 9 - 0
app/assets/stylesheets/scenarios.css.scss

@@ -0,0 +1,9 @@
1
+.scenario-import {
2
+  .danger {
3
+    color: red;
4
+    font-weight: strong;
5
+    border: 1px solid red;
6
+    padding: 10px;
7
+    margin: 10px 0;
8
+  }
9
+}

+ 1 - 1
app/concerns/assignable_types.rb

@@ -29,7 +29,7 @@ module AssignableTypes
29 29
       const_get(:TYPES).include?(type)
30 30
     end
31 31
 
32
-    def build_for_type(type, user, attributes)
32
+    def build_for_type(type, user, attributes = {})
33 33
       attributes.delete(:type)
34 34
 
35 35
       if valid_type?(type)

+ 13 - 0
app/concerns/has_guid.rb

@@ -0,0 +1,13 @@
1
+module HasGuid
2
+  extend ActiveSupport::Concern
3
+
4
+  included do
5
+    before_save :make_guid
6
+  end
7
+
8
+  protected
9
+
10
+  def make_guid
11
+    self.guid = SecureRandom.hex unless guid.present?
12
+  end
13
+end

+ 1 - 0
app/models/agent.rb

@@ -12,6 +12,7 @@ class Agent < ActiveRecord::Base
12 12
   include JSONSerializedField
13 13
   include RDBMSFunctions
14 14
   include WorkingHelpers
15
+  include HasGuid
15 16
 
16 17
   markdown_class_attributes :description, :event_description
17 18
 

+ 3 - 7
app/models/scenario.rb

@@ -1,22 +1,18 @@
1 1
 class Scenario < ActiveRecord::Base
2
-  attr_accessible :name, :agent_ids, :description, :public
2
+  include HasGuid
3
+
4
+  attr_accessible :name, :agent_ids, :description, :public, :source_url
3 5
 
4 6
   belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios
5 7
   has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario
6 8
   has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios
7 9
 
8
-  before_save :make_guid
9
-
10 10
   validates_presence_of :name, :user
11 11
 
12 12
   validate :agents_are_owned
13 13
 
14 14
   protected
15 15
 
16
-  def make_guid
17
-    self.guid = SecureRandom.hex unless guid.present?
18
-  end
19
-
20 16
   def agents_are_owned
21 17
     errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user }
22 18
   end

+ 42 - 4
app/models/scenario_import.rb

@@ -4,6 +4,7 @@ class ScenarioImport
4 4
   include ActiveModel::Callbacks
5 5
   include ActiveModel::Validations::Callbacks
6 6
 
7
+  DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent]
7 8
   URL_REGEX = /\Ahttps?:\/\//i
8 9
 
9 10
   attr_accessor :file, :url, :data, :do_import
@@ -33,19 +34,54 @@ class ScenarioImport
33 34
     @existing_scenario ||= user.scenarios.find_by_guid(parsed_data["guid"])
34 35
   end
35 36
 
37
+  def dangerous?
38
+    (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) }
39
+  end
40
+
36 41
   def parsed_data
37
-    @parsed_data
42
+    @parsed_data ||= data && JSON.parse(data) rescue {}
38 43
   end
39 44
 
40 45
   def do_import?
41 46
     do_import == "1"
42 47
   end
43 48
 
44
-  def import!
49
+  def import!(options = {})
50
+    guid = parsed_data['guid']
51
+    description = parsed_data['description']
52
+    name = parsed_data['name']
53
+    agents = parsed_data['agents']
54
+    links = parsed_data['links']
55
+    source_url = parsed_data['source_url'].presence || nil
56
+    @scenario = user.scenarios.where(:guid => guid).first_or_initialize
57
+    @scenario.update_attributes!(:name => name, :description => description,
58
+                                 :source_url => source_url, :public => false)
59
+
60
+    unless options[:skip_agents]
61
+      created_agents = agents.map do |agent_data|
62
+        agent = @scenario.agents.find_by(:guid => agent_data['guid']) || Agent.build_for_type(agent_data['type'], user)
63
+        agent.guid = agent_data['guid']
64
+        agent.attributes = { :name => agent_data['name'],
65
+                             :schedule => agent_data['schedule'],
66
+                             :keep_events_for => agent_data['keep_events_for'],
67
+                             :propagate_immediately => agent_data['propagate_immediately'],
68
+                             :disabled => agent_data['disabled'],
69
+                             :options => agent_data['options'],
70
+                             :scenario_ids => [@scenario.id] }
71
+        agent.save!
72
+        agent
73
+      end
74
+
75
+      links.each do |link|
76
+        receiver = created_agents[link['receiver']]
77
+        source = created_agents[link['source']]
78
+        receiver.sources << source unless receiver.sources.include?(source)
79
+      end
80
+    end
45 81
   end
46 82
 
47 83
   def scenario
48
-    existing_scenario
84
+    @scenario || @existing_scenario
49 85
   end
50 86
 
51 87
   protected
@@ -65,10 +101,12 @@ class ScenarioImport
65 101
   def validate_data
66 102
     if data.present?
67 103
       @parsed_data = JSON.parse(data) rescue {}
68
-      if (%w[name guid] - @parsed_data.keys).length > 0
104
+      if (%w[name guid agents] - @parsed_data.keys).length > 0
69 105
         errors.add(:base, "The provided data does not appear to be a valid Scenario.")
70 106
         self.data = nil
71 107
       end
108
+    else
109
+      @parsed_data = nil
72 110
     end
73 111
   end
74 112
 

+ 13 - 4
app/views/scenario_imports/_step_two.html.erb

@@ -32,11 +32,20 @@
32 32
     });
33 33
   </script>
34 34
 
35
+  <% if @scenario_import.dangerous? %>
36
+    <div class="danger">
37
+      This Scenario contains one or more potentially dangerous Agents.
38
+      These may be able to run local commands or execute code.
39
+      Please be sure that you understand the above Agent configurations before importing!
40
+    </div>
41
+  <% end %>
42
+
35 43
   <% if @scenario_import.existing_scenario.present? %>
36
-    <strong>
37
-      This Scenario already exists on your Huginn.
38
-      If you continue, the import will overwrite your existing <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario.
39
-    </strong>
44
+    <div class="danger">
45
+      This Scenario already exists in your system.
46
+      If you continue, the import will overwrite your existing
47
+      <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario and the Agents in it.
48
+    </div>
40 49
   <% end %>
41 50
 
42 51
   <div class="checkbox">

+ 1 - 1
app/views/scenario_imports/new.html.erb

@@ -1,4 +1,4 @@
1
-<div class='container'>
1
+<div class='container scenario-import'>
2 2
   <div class='row'>
3 3
     <div class='col-md-12'>
4 4
       <% if @scenario_import.errors.any? %>

+ 2 - 0
app/views/scenarios/index.html.erb

@@ -13,6 +13,7 @@
13 13
         <tr>
14 14
           <th>Name</th>
15 15
           <th>Agents</th>
16
+          <th>Public</th>
16 17
           <th></th>
17 18
         </tr>
18 19
 
@@ -22,6 +23,7 @@
22 23
               <%= link_to(scenario.name, scenario, class: "label label-info") %>
23 24
             </td>
24 25
             <td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td>
26
+            <td><%= scenario.public? ? "yes" : "no" %></td>
25 27
             <td>
26 28
               <div class="btn-group btn-group-xs" style="float: right">
27 29
                 <%= link_to 'Show', scenario, class: "btn btn-default" %>

+ 1 - 1
db/migrate/20140602014917_add_indices_to_scenarios.rb

@@ -1,6 +1,6 @@
1 1
 class AddIndicesToScenarios < ActiveRecord::Migration
2 2
   def change
3
-    add_index :scenarios, [:user_id, :guid]
3
+    add_index :scenarios, [:user_id, :guid], :unique => true
4 4
     add_index :scenario_memberships, :agent_id
5 5
     add_index :scenario_memberships, :scenario_id
6 6
   end

+ 15 - 0
db/migrate/20140605032822_add_guid_to_agents.rb

@@ -0,0 +1,15 @@
1
+class AddGuidToAgents < ActiveRecord::Migration
2
+  class Agent < ActiveRecord::Base; end
3
+
4
+  def change
5
+    add_column :agents, :guid, :string
6
+
7
+    Agent.find_each do |agent|
8
+      agent.update_attribute :guid, SecureRandom.hex
9
+    end
10
+
11
+    change_column_null :agents, :guid, false
12
+
13
+    add_index :agents, :guid
14
+  end
15
+end

+ 4 - 2
db/schema.rb

@@ -11,7 +11,7 @@
11 11
 #
12 12
 # It's strongly recommended that you check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(version: 20140602014917) do
14
+ActiveRecord::Schema.define(version: 20140605032822) do
15 15
 
16 16
   create_table "agent_logs", force: true do |t|
17 17
     t.integer  "agent_id",                                       null: false
@@ -42,8 +42,10 @@ ActiveRecord::Schema.define(version: 20140602014917) do
42 42
     t.integer  "keep_events_for",                          default: 0,     null: false
43 43
     t.boolean  "propagate_immediately",                    default: false, null: false
44 44
     t.boolean  "disabled",                                 default: false, null: false
45
+    t.string   "guid",                                                     null: false
45 46
   end
46 47
 
48
+  add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree
47 49
   add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree
48 50
   add_index "agents", ["type"], name: "index_agents_on_type", using: :btree
49 51
   add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree
@@ -111,7 +113,7 @@ ActiveRecord::Schema.define(version: 20140602014917) do
111 113
     t.string   "source_url"
112 114
   end
113 115
 
114
-  add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", using: :btree
116
+  add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree
115 117
 
116 118
   create_table "user_credentials", force: true do |t|
117 119
     t.integer  "user_id",                           null: false

+ 1 - 1
lib/agents_exporter.rb

@@ -46,7 +46,7 @@ class AgentsExporter
46 46
       :keep_events_for => agent.keep_events_for,
47 47
       :propagate_immediately => agent.propagate_immediately,
48 48
       :disabled => agent.disabled,
49
-      :source_system_agent_id => agent.id,
49
+      :guid => agent.guid,
50 50
       :options => agent.options
51 51
     }
52 52
   end

+ 8 - 0
spec/fixtures/agents.yml

@@ -4,6 +4,7 @@ jane_website_agent:
4 4
   events_count: 1
5 5
   schedule: "5pm"
6 6
   name: "ZKCD"
7
+  guid: <%= SecureRandom.hex %>
7 8
   options: <%= {
8 9
                  :url => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss",
9 10
                  :expected_update_period_in_days => 2,
@@ -20,6 +21,7 @@ bob_website_agent:
20 21
   events_count: 1
21 22
   schedule: "midnight"
22 23
   name: "ZKCD"
24
+  guid: <%= SecureRandom.hex %>
23 25
   options: <%= {
24 26
                  :url => "http://xkcd.com",
25 27
                  :expected_update_period_in_days => 2,
@@ -35,6 +37,7 @@ bob_weather_agent:
35 37
   user: bob
36 38
   schedule: "midnight"
37 39
   name: "SF Weather"
40
+  guid: <%= SecureRandom.hex %>
38 41
   keep_events_for: 45
39 42
   options: <%= {
40 43
                  :location => 94102,
@@ -48,6 +51,7 @@ jane_weather_agent:
48 51
   user: jane
49 52
   schedule: "midnight"
50 53
   name: "SF Weather"
54
+  guid: <%= SecureRandom.hex %>
51 55
   keep_events_for: 30
52 56
   options: <%= {
53 57
                  :location => 94103,
@@ -60,6 +64,7 @@ jane_rain_notifier_agent:
60 64
   type: Agents::TriggerAgent
61 65
   user: jane
62 66
   name: "Jane's Rain Watcher"
67
+  guid: <%= SecureRandom.hex %>
63 68
   options: <%= {
64 69
                  :expected_receive_period_in_days => "2",
65 70
                  :rules => [{
@@ -74,6 +79,7 @@ bob_rain_notifier_agent:
74 79
   type: Agents::TriggerAgent
75 80
   user: bob
76 81
   name: "Bob's Rain Watcher"
82
+  guid: <%= SecureRandom.hex %>
77 83
   options: <%= {
78 84
                  :expected_receive_period_in_days => "2",
79 85
                  :rules => [{
@@ -88,6 +94,7 @@ bob_twitter_user_agent:
88 94
   type: Agents::TwitterUserAgent
89 95
   user: bob
90 96
   name: "Bob's Twitter User Watcher"
97
+  guid: <%= SecureRandom.hex %>
91 98
   options: <%= {
92 99
       :username => "tectonic",
93 100
       :expected_update_period_in_days => "2",
@@ -101,3 +108,4 @@ bob_manual_event_agent:
101 108
   type: Agents::ManualEventAgent
102 109
   user: bob
103 110
   name: "Bob's event testing agent"
111
+  guid: <%= SecureRandom.hex %>

+ 1 - 1
spec/lib/agents_exporter_spec.rb

@@ -20,7 +20,7 @@ describe AgentsExporter do
20 20
       Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc)
21 21
       data[:links].should == [{ :source => 0, :receiver => 1 }]
22 22
       data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) }
23
-      data[:agents].all? { |agent_json| agent_json[:source_system_agent_id] && agent_json[:type] && agent_json[:name] }.should be_true
23
+      data[:agents].all? { |agent_json| agent_json[:guid].present? && agent_json[:type].present? && agent_json[:name].present? }.should be_true
24 24
     end
25 25
 
26 26
     it "does not output links to other agents" do

+ 8 - 1
spec/models/agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/working_helpers'
3 2
 
4 3
 describe Agent do
5 4
   it_behaves_like WorkingHelpers
@@ -122,6 +121,14 @@ describe Agent do
122 121
       stub(Agents::CannotBeScheduled).valid_type?("Agents::CannotBeScheduled") { true }
123 122
     end
124 123
 
124
+    let(:new_instance) do
125
+      agent = Agents::SomethingSource.new(:name => "some agent")
126
+      agent.user = users(:bob)
127
+      agent
128
+    end
129
+
130
+    it_behaves_like HasGuid
131
+
125 132
     describe ".default_schedule" do
126 133
       it "stores the default on the class" do
127 134
         Agents::SomethingSource.default_schedule.should == "2pm"

+ 0 - 1
spec/models/agents/data_output_agent_spec.rb

@@ -1,7 +1,6 @@
1 1
 # encoding: utf-8
2 2
 
3 3
 require 'spec_helper'
4
-require 'models/concerns/liquid_interpolatable'
5 4
 
6 5
 describe Agents::DataOutputAgent do
7 6
   it_behaves_like LiquidInterpolatable

+ 0 - 1
spec/models/agents/hipchat_agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3 2
 
4 3
 describe Agents::HipchatAgent do
5 4
   it_behaves_like LiquidInterpolatable

+ 0 - 1
spec/models/agents/human_task_agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3 2
 
4 3
 describe Agents::HumanTaskAgent do
5 4
   it_behaves_like LiquidInterpolatable

+ 0 - 1
spec/models/agents/jabber_agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3 2
 
4 3
 describe Agents::JabberAgent do
5 4
   it_behaves_like LiquidInterpolatable

+ 0 - 1
spec/models/agents/peak_detector_agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3 2
 
4 3
 describe Agents::PeakDetectorAgent do
5 4
   it_behaves_like LiquidInterpolatable

+ 0 - 1
spec/models/agents/pushbullet_agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3 2
 
4 3
 describe Agents::PushbulletAgent do
5 4
   it_behaves_like LiquidInterpolatable

+ 0 - 1
spec/models/agents/slack_agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3 2
 
4 3
 describe Agents::SlackAgent do
5 4
   it_behaves_like LiquidInterpolatable

+ 0 - 2
spec/models/agents/translation_agent_spec.rb

@@ -1,6 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3
-
4 2
 
5 3
 describe Agents::TranslationAgent do
6 4
     it_behaves_like LiquidInterpolatable

+ 0 - 1
spec/models/agents/trigger_agent_spec.rb

@@ -1,5 +1,4 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/liquid_interpolatable'
3 2
 
4 3
 describe Agents::TriggerAgent do
5 4
   it_behaves_like LiquidInterpolatable

+ 188 - 8
spec/models/scenario_import_spec.rb

@@ -1,6 +1,64 @@
1 1
 require 'spec_helper'
2 2
 
3 3
 describe ScenarioImport do
4
+  let(:guid) { "somescenarioguid" }
5
+  let(:description) { "This is a cool Huginn Scenario that does something useful!" }
6
+  let(:name) { "A useful Scenario" }
7
+  let(:source_url) { "http://example.com/scenarios/2/export.json" }
8
+  let(:weather_agent_options) {
9
+    {
10
+      'api_key' => 'some-api-key',
11
+      'location' => '12345'
12
+    }
13
+  }
14
+  let(:trigger_agent_options) {
15
+    {
16
+      'expected_receive_period_in_days' => 2,
17
+      'rules' => [{
18
+                    'type' => "regex",
19
+                    'value' => "rain|storm",
20
+                    'path' => "conditions",
21
+                  }],
22
+      'message' => "Looks like rain!"
23
+    }
24
+  }
25
+  let(:valid_parsed_data) do
26
+    { 
27
+      :name => name,
28
+      :description => description,
29
+      :guid => guid,
30
+      :source_url => source_url,
31
+      :exported_at => 2.days.ago.utc.iso8601,
32
+      :agents => [
33
+        {
34
+          :type => "Agents::WeatherAgent",
35
+          :name => "a weather agent",
36
+          :schedule => "5pm",
37
+          :keep_events_for => 14,
38
+          :propagate_immediately => false,
39
+          :disabled => false,
40
+          :guid => "a-weather-agent",
41
+          :options => weather_agent_options
42
+        },
43
+        {
44
+          :type => "Agents::TriggerAgent",
45
+          :name => "listen for weather",
46
+          :schedule => nil,
47
+          :keep_events_for => 0,
48
+          :propagate_immediately => true,
49
+          :disabled => true,
50
+          :guid => "a-trigger-agent",
51
+          :options => trigger_agent_options
52
+        }
53
+      ],
54
+      :links => [
55
+        { :source => 0, :receiver => 1 }
56
+      ]
57
+    }
58
+  end
59
+  let(:valid_data) { valid_parsed_data.to_json }
60
+  let(:invalid_data) { { :name => "some scenario missing a guid" }.to_json }
61
+
4 62
   describe "initialization" do
5 63
     it "is initialized with an attributes hash" do
6 64
       ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com"
@@ -9,8 +67,6 @@ describe ScenarioImport do
9 67
 
10 68
   describe "validations" do
11 69
     subject { ScenarioImport.new }
12
-    let(:valid_json) { { :name => "some scenario", :guid => "someguid" }.to_json }
13
-    let(:invalid_json) { { :name => "some scenario missing a guid" }.to_json }
14 70
 
15 71
     it "is not valid when none of file, url, or data are present" do
16 72
       subject.should_not be_valid
@@ -20,7 +76,7 @@ describe ScenarioImport do
20 76
 
21 77
     describe "data" do
22 78
       it "should be invalid with invalid data" do
23
-        subject.data = invalid_json
79
+        subject.data = invalid_data
24 80
         subject.should_not be_valid
25 81
         subject.should have(1).error_on(:base)
26 82
 
@@ -33,7 +89,7 @@ describe ScenarioImport do
33 89
       end
34 90
 
35 91
       it "should be valid with valid data" do
36
-        subject.data = valid_json
92
+        subject.data = valid_data
37 93
         subject.should be_valid
38 94
       end
39 95
     end
@@ -47,14 +103,14 @@ describe ScenarioImport do
47 103
       end
48 104
 
49 105
       it "should be invalid when the referenced url doesn't contain a scenario" do
50
-        stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_json)
106
+        stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_data)
51 107
         subject.url = "http://example.com/scenarios/1/export.json"
52 108
         subject.should_not be_valid
53 109
         subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.")
54 110
       end
55 111
 
56 112
       it "should be valid when the url points to a valid scenario" do
57
-        stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_json)
113
+        stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_data)
58 114
         subject.url = "http://example.com/scenarios/1/export.json"
59 115
         subject.should be_valid
60 116
       end
@@ -66,15 +122,139 @@ describe ScenarioImport do
66 122
         subject.should_not be_valid
67 123
         subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.")
68 124
 
69
-        subject.file = StringIO.new(invalid_json)
125
+        subject.file = StringIO.new(invalid_data)
70 126
         subject.should_not be_valid
71 127
         subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.")
72 128
       end
73 129
 
74 130
       it "should be valid with a valid uploaded scenario" do
75
-        subject.file = StringIO.new(valid_json)
131
+        subject.file = StringIO.new(valid_data)
76 132
         subject.should be_valid
77 133
       end
78 134
     end
79 135
   end
136
+  
137
+  describe "#dangerous?" do
138
+    it "returns false on most Agents" do
139
+      ScenarioImport.new(:data => valid_data).should_not be_dangerous
140
+    end
141
+
142
+    it "returns true if a ShellCommandAgent is present" do
143
+      valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent"
144
+      ScenarioImport.new(:data => valid_parsed_data.to_json).should be_dangerous
145
+    end
146
+  end
147
+
148
+  describe "#import!" do
149
+    let(:scenario_import) do
150
+      _import = ScenarioImport.new(:data => valid_data)
151
+      _import.set_user users(:bob)
152
+      _import
153
+    end
154
+
155
+    context "when this scenario has never been seen before" do
156
+      it "makes a new scenario" do
157
+        lambda {
158
+          scenario_import.import!(:skip_agents => true)
159
+        }.should change { users(:bob).scenarios.count }.by(1)
160
+
161
+        scenario_import.scenario.name.should == name
162
+        scenario_import.scenario.description.should == description
163
+        scenario_import.scenario.guid.should == guid
164
+        scenario_import.scenario.source_url.should == source_url
165
+        scenario_import.scenario.public.should be_false
166
+      end
167
+
168
+      it "creates the Agents" do
169
+        lambda {
170
+          scenario_import.import!
171
+        }.should change { users(:bob).agents.count }.by(2)
172
+
173
+        weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent")
174
+        trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent")
175
+
176
+        weather_agent.name.should == "a weather agent"
177
+        weather_agent.schedule.should == "5pm"
178
+        weather_agent.keep_events_for.should == 14
179
+        weather_agent.propagate_immediately.should be_false
180
+        weather_agent.should_not be_disabled
181
+        weather_agent.memory.should be_empty
182
+        weather_agent.options.should == weather_agent_options
183
+
184
+        trigger_agent.name.should == "listen for weather"
185
+        trigger_agent.sources.should == [weather_agent]
186
+        trigger_agent.schedule.should be_nil
187
+        trigger_agent.keep_events_for.should == 0
188
+        trigger_agent.propagate_immediately.should be_true
189
+        trigger_agent.should be_disabled
190
+        trigger_agent.memory.should be_empty
191
+        trigger_agent.options.should == trigger_agent_options
192
+      end
193
+
194
+      it "creates new Agents, even if one already exists with the given guid (so that we don't overwrite a user's work outside of the scenario)" do
195
+        agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
196
+
197
+        lambda {
198
+          scenario_import.import!
199
+        }.should change { users(:bob).agents.count }.by(2)
200
+      end
201
+    end
202
+
203
+    context "when an a scenario already exists with the given guid" do
204
+      let!(:existing_scenario) {
205
+        _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario")
206
+        _existing_scenerio.guid = guid
207
+        _existing_scenerio.save!
208
+        _existing_scenerio
209
+      }
210
+
211
+      it "uses the existing scenario, updating it's data" do
212
+        lambda {
213
+          scenario_import.import!(:skip_agents => true)
214
+          scenario_import.scenario.should == existing_scenario
215
+        }.should_not change { users(:bob).scenarios.count }
216
+
217
+        existing_scenario.reload
218
+        existing_scenario.guid.should == guid
219
+        existing_scenario.description.should == description
220
+        existing_scenario.name.should == name
221
+        existing_scenario.source_url.should == source_url
222
+        existing_scenario.public.should be_false
223
+      end
224
+
225
+      it "updates any existing agents in the scenario, and makes new ones as needed" do
226
+        agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
227
+        agents(:bob_weather_agent).scenarios << existing_scenario
228
+
229
+        lambda {
230
+          # Shouldn't matter how many times we do it!
231
+          scenario_import.import!
232
+          scenario_import.import!
233
+          scenario_import.import!
234
+        }.should change { users(:bob).agents.count }.by(1)
235
+
236
+        weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent")
237
+        trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent")
238
+
239
+        weather_agent.should == agents(:bob_weather_agent)
240
+
241
+        weather_agent.name.should == "a weather agent"
242
+        weather_agent.schedule.should == "5pm"
243
+        weather_agent.keep_events_for.should == 14
244
+        weather_agent.propagate_immediately.should be_false
245
+        weather_agent.should_not be_disabled
246
+        weather_agent.memory.should be_empty
247
+        weather_agent.options.should == weather_agent_options
248
+
249
+        trigger_agent.name.should == "listen for weather"
250
+        trigger_agent.sources.should == [weather_agent]
251
+        trigger_agent.schedule.should be_nil
252
+        trigger_agent.keep_events_for.should == 0
253
+        trigger_agent.propagate_immediately.should be_true
254
+        trigger_agent.should be_disabled
255
+        trigger_agent.memory.should be_empty
256
+        trigger_agent.options.should == trigger_agent_options
257
+      end
258
+    end
259
+  end
80 260
 end

+ 15 - 27
spec/models/scenario_spec.rb

@@ -1,54 +1,42 @@
1 1
 require 'spec_helper'
2 2
 
3 3
 describe Scenario do
4
+  let(:new_instance) { users(:bob).scenarios.build(:name => "some scenario") }
5
+
6
+  it_behaves_like HasGuid
7
+
4 8
   describe "validations" do
5 9
     before do
6
-      @scenario = users(:bob).scenarios.new(:name => "some scenario")
7
-      @scenario.should be_valid
10
+      new_instance.should be_valid
8 11
     end
9 12
 
10 13
     it "validates the presence of name" do
11
-      @scenario.name = ''
12
-      @scenario.should_not be_valid
14
+      new_instance.name = ''
15
+      new_instance.should_not be_valid
13 16
     end
14 17
 
15 18
     it "validates the presence of user" do
16
-      @scenario.user = nil
17
-      @scenario.should_not be_valid
19
+      new_instance.user = nil
20
+      new_instance.should_not be_valid
18 21
     end
19 22
 
20 23
     it "only allows Agents owned by user" do
21
-      @scenario.agent_ids = [agents(:bob_website_agent).id]
22
-      @scenario.should be_valid
24
+      new_instance.agent_ids = [agents(:bob_website_agent).id]
25
+      new_instance.should be_valid
23 26
 
24
-      @scenario.agent_ids = [agents(:jane_website_agent).id]
25
-      @scenario.should_not be_valid
26
-    end
27
-  end
28
-
29
-  describe "guid" do
30
-    it "gets created before_save, but only if it's not present" do
31
-      scenario = users(:bob).scenarios.new(:name => "some scenario")
32
-      scenario.guid.should be_nil
33
-      scenario.save!
34
-      scenario.guid.should_not be_nil
35
-
36
-      lambda { scenario.save! }.should_not change { scenario.reload.guid }
27
+      new_instance.agent_ids = [agents(:jane_website_agent).id]
28
+      new_instance.should_not be_valid
37 29
     end
38 30
   end
39 31
 
40 32
   describe "counters" do
41
-    before do
42
-      @scenario = users(:bob).scenarios.new(:name => "some scenario")
43
-    end
44
-
45 33
     it "maintains a counter cache on user" do
46 34
       lambda {
47
-        @scenario.save!
35
+        new_instance.save!
48 36
       }.should change { users(:bob).reload.scenario_count }.by(1)
49 37
 
50 38
       lambda {
51
-        @scenario.destroy
39
+        new_instance.destroy
52 40
       }.should change { users(:bob).reload.scenario_count }.by(-1)
53 41
     end
54 42
   end

+ 12 - 0
spec/support/shared_examples/has_guid.rb

@@ -0,0 +1,12 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for HasGuid do
4
+  it "gets created before_save, but only if it's not present" do
5
+    instance = new_instance
6
+    instance.guid.should be_nil
7
+    instance.save!
8
+    instance.guid.should_not be_nil
9
+
10
+    lambda { instance.save! }.should_not change { instance.reload.guid }
11
+  end
12
+end

spec/models/concerns/liquid_interpolatable.rb → spec/support/shared_examples/liquid_interpolatable.rb


+ 3 - 3
spec/models/concerns/working_helpers.rb

@@ -3,7 +3,7 @@ require 'spec_helper'
3 3
 shared_examples_for WorkingHelpers do
4 4
   describe "recent_error_logs?" do
5 5
     it "returns true if last_error_log_at is near last_event_at" do
6
-      agent = Agent.new
6
+      agent = described_class.new
7 7
 
8 8
       agent.last_error_log_at = 10.minutes.ago
9 9
       agent.last_event_at = 10.minutes.ago
@@ -26,9 +26,10 @@ shared_examples_for WorkingHelpers do
26 26
       agent.recent_error_logs?.should be_false
27 27
     end
28 28
   end
29
+
29 30
   describe "received_event_without_error?" do
30 31
     before do
31
-      @agent = Agent.new
32
+      @agent = described_class.new
32 33
     end
33 34
 
34 35
     it "should return false until the first event was received" do
@@ -49,5 +50,4 @@ shared_examples_for WorkingHelpers do
49 50
       @agent.received_event_without_error?.should == true
50 51
     end
51 52
   end
52
-
53 53
 end